#pragma once

#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"

#include <signal.h>
#include <unistd.h>
#include <jsoncpp/json/json.h>

namespace ns_compile_and_run
{
    using namespace ns_log;
    using namespace ns_util;
    using namespace ns_compiler;
    using namespace ns_runner;

    class CompileAndRun
    {
    public:
        // 成功输出结果之后，我们需要清理在这个过程中产生的所有临时文件，否则产生堆积会造成空间的浪费

        // 那中间产生的临时文件有哪些呢？
            // 编译时所需的src文件，
            // 编译后产生的file_name.exe文件，
            // 执行过程中产生的.stdin/.stdout/.stderr文件

        // 如何清理这些临时文件呢？
            // 使用系统调用unlink来删
            // unlink的基本用法
                // #include <unistd.h>
                // int unlink(const char *pathname);
                // 参数：pathname 是要删除的文件路径。
                // 返回值：调用成功时返回 0，失败则返回 -1，同时会设置 errno 来表明具体错误。
            // unlink的执行效果
                // 如果pathname指向的文件是硬链接文件，那每执行一次 unlink，文件的链接数就会减 1。当链接数降到 0 时，文件才会被真正删除。
                // 如果pathname指向的文件是符号链接文件，那么被删除的是符号链接本身，而不是它所指向的目标文件。
                // 已打开文件：即便文件被 unlink 了，只要还有进程处于打开该文件的状态，文件就会一直存在，直到所有打开它的进程都关闭文件描述符。
                // 目录限制：在大多数文件系统中，普通用户无法直接对目录使用 unlink，而是需要借助 rmdir 系统调用。
        static void RemoveTempFile(const std::string &file_name)
        {
            // 为什么清理文件的个数是不确定的？
                // 因为如果用户提交的代码，编译就没通过，就不会生成后续的可执行文件，后续也就不会创建该可执行文件的标准输入输出流文件
            //由于清理文件的个数是不确定的，所以我们在清理之前需要先查找一下这个文件到底有没有，如果有，我们再删除

            // 首先我们根据PathUtil::Src(file_name)，获得临时源文件的完整路径
            // 然后再通过FileUtil::IsFileExists查询源文件是否真的存在，如果存在，就用unlink删除源文件
            std::string _src = PathUtil::Src(file_name);
            if(FileUtil::IsFileExists(_src)) unlink(_src.c_str());

            // 检查编译报错文件是否存在，如果有就删了
            std::string _compiler_error = PathUtil::CompilerError(file_name);
            if(FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_error.c_str());

            // 检查可执行程序文件是否存在，如果有就删了
            std::string _execute = PathUtil::Exe(file_name);
            if(FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());

            // 检查程序运行时的标准输入输出流文件是否存在，如果有就删了
            std::string _stdin = PathUtil::Stdin(file_name);
            if(FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());

            std::string _stdout = PathUtil::Stdout(file_name);
            if(FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());

            std::string _stderr = PathUtil::Stderr(file_name);
            if(FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());
        }
        
        // 这个函数用来输入的状态码，返回对应的错误原因
        static std::string CodeToDesc(int code, const std::string &file_name)
        {
        // code > 0 : 进程收到了信号导致异常奔溃
        // code < 0 : 整个过程非运行报错(代码为空，编译报错等)
        // code = 0 : 整个过程全部完成
        //待完善
            std::string desc;
            switch (code)
            {
            case 0:
                desc = "编译运行成功";
                break;
            case -1:
                desc = "提交的代码是空";
                break;
            case -2:
                desc = "未知错误";
                break;
            case -3:
                // desc = "代码编译的时候发生了错误";
                // 下面我们就想把编译的报错信息写入desc中，那哪里有编译的报错信息呢？
                    // 答案是在编译的时候，我们会将编译的报错信息重定向输出到一个.compile_error文件中
                // 所以接下来我们只需要读取file_name.compile_error文件中的信息，并将其写入到desc中即可
                FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);
                break;
            case SIGABRT: // 6
                desc = "内存超过范围";
                break;
            case SIGXCPU: // 24
                desc = "CPU使用超时";
                break;
            case SIGFPE: // 8
                desc = "浮点数溢出";
                break;
            default:
                desc = "未知: " + std::to_string(code);
                break;
            }

            return desc;
        }

        /***************************************
         * Start函数的输入:
         * code： 用户提交的代码
         * input: 用户给自己提交的代码对应的输入，不做处理
         * cpu_limit: 时间要求
         * mem_limit: 空间要求
         *
         * Start函数的输出:
         * 必填
         * status: 状态码
         * reason: 请求结果
         * 选填：
         * stdout: 我的程序运行完的结果
         * stderr: 我的程序运行完的错误结果
         *
         * 根据Start函数的输入输出数据，确定输入输出的json字符串的格式：
         * in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
         * out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
         * ************************************/
        static void Start(const std::string &in_json, std::string *out_json)
        {
            // 首先我们要做的工作是反序列化，即：将json字符串in_json中的各个参数提取出来
            Json::Value in_value;
            // 创建一个Json::Value对象in_value
            // Json::Value是 Jsoncpp 库中表示 JSON 数据的核心类。可以存储各种 JSON 类型（对象、数组、字符串、数字等），类似于一个动态类型的容器。
            Json::Reader reader;
            // 创建一个Json::Reader对象reader，Reader中的函数可以用于解析 JSON 字符串。
            reader.parse(in_json, in_value); //最后再处理差错问题
            // 调用reader的parse方法，尝试将输入的 JSON 字符串in_json解析到in_value中。
            // 假如说in_json={"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}，且调用reader.parse之前 reader为空
            // 调用reader.parse之后，in_value中就以键值对的形式存储in_json中的一条条数据，和哈希表非常像
            std::string code = in_value["code"].asString();
            // in_value["code"]的类型依然是Json::Value ，但实际上其实里面存的就是一个string类型的字符串
            // 这种情况我们只需调用Json::Value提供给我们的接口asString()，将其强转成string类型即可
            // 下面几句代码也是同理
            std::string input = in_value["input"].asString();
            int cpu_limit = in_value["cpu_limit"].asInt();
            int mem_limit = in_value["mem_limit"].asInt();

            int status_code = 0;
            Json::Value out_value;
            int run_result = 0;
            std::string file_name; //需要内部形成的唯一文件名

            // code里面存储的是用户提交上来的代码
            if (code.size() == 0)
            {
                // 走到这里就说明代码是空的
                status_code = -1; 
                // status_code = -1就对应着code为空的错误
                goto END;
            }

            // code里面存储的不是用户提交上来的代码嘛，那这段代码最终肯定是要存到一个cpp文件(src源文件)中吧
            // 那这个cpp文件总得有个名字吧，那这个名字应该怎么起呢？我们说只要能保证名字不重复，你随便起
                // 即保证形成的文件名具有唯一性，由于我们后面有专门的函数为其加上路径前缀和文件后缀，所以此时起名字就不用考虑这些
            // 那如何做到起的名字具有唯一性呢？
                // 我们用毫秒级时间戳+原子性递增唯一值：来保证唯一性
            file_name = FileUtil::UniqFileName();

            LOG(INFO)<< "形成的临时src文件名是"<<file_name<<"\n";
            LOG(INFO)<< "其包含路径的完整文件名是"<<PathUtil::Src(file_name)<<"\n";
            // 形成临时src文件
            if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
            {
                // PathUtil::Src(file_name)是为该file_name形成一个包含完整路径前缀和文件后缀的完整文件名
                // code里面存储的是用户提交上来的代码
                // FileUtil::WriteFile这个函数负责在PathUtil::Src(file_name)指向的位置创建该文件，并将用户提交的代码code写入其中

                // 走到这里就说明临时文件创建失败了
                // 但是按照常理来说，我临时文件肯定是能创建成功的，所以我并不能确定此处出错的原因
                status_code = -2; 
                LOG(INFO) << "临时src文件创建失败，直接返回"<< "\n";
                // status_code = -2对应未知错误
                goto END;
            }

            // 走到这里说明临时src文件成功生成，接下来我们就要开始对其进行编译了
            if (!Compiler::Compile(file_name))
            {
                // 走到这里就说明编译失败了
                status_code = -3; 
                // status_code = -3对应着编译失败的错误
                goto END;
            }

            // 走到这里说明编译也成功了，已经成功形成可执行文件了，下面我们就要执行这个可执行文件了
            run_result = Runner::Run(file_name, cpu_limit, mem_limit);
            // Run返回的是该可执行文件执行之后的退出码，如果为0 ，说明正常退出，不为0说明异常退出
            // 下面我们就用 变量status_code来接收该可执行文件执行之后的退出码
            if (run_result < 0)
            {
                // Run()返回的结果小于0，原因可能是子进程创建失败，或者该进程的三个标准流文件创建失败了
                // 反正不管具体是哪一种，只要状态码返回的是负数，那都不是用户的问题，而是服务器本身的问题
                status_code = -2; //未知错误
                LOG(INFO) << "Run()返回的结果小于0，原因可能是子进程创建失败，或者该进程的三个标准流文件创建失败了"
                << "\n"<<"反正不管具体是哪一种，只要状态码返回的是负数，那都不是用户的问题，而是服务器本身的问题"
                <<"\n";
            }
            else if (run_result > 0)
            {
                // 说明程序运行崩溃了
                // 此时Runner::Run()返回的run_result，里面存的就是让该进程异常终止的信号编号
                status_code = run_result;
            }
            else
            {
                //运行成功
                status_code = 0;
                // status_code = 0说明运行成功
            }
        END:
            // out_value是一个输出型参数，out_json格式如下: {"status":"0", "reason":"","stdout":"","stderr":"",}
            // 我们要对外输出：1.子进程退出的状态码。2.进程退出原因。3.
            out_value["status"] = status_code;

            // CodeToDesc()函数会根据退出码生成对应的报错原因
            out_value["reason"] = CodeToDesc(status_code, file_name);

            // 如果状态码为0，说明子进程执行完毕之后正常退出
            if (status_code == 0)
            {
                // 走到这里说明我们成功编译并运行了用户提交的代码，整个过程全部成功

                // 用户提交代码执行完毕之后，不是会输出一些结果嘛，这些结果前面已经被我们重定向输出到了file_name.stdout文件中
                // 现在我们就想将这个文件的输出结果读出来，用string类型的变量_stdout来接收
                std::string _stdout;
                FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);
                out_value["stdout"] = _stdout;

                // 用户提交代码在执行过程中也可能会报错，比如超时或者除零错误啥的，这些结果前面已经被我们重定向输出到了file_name.stderr文件中
                // 现在我们就像将这个文件中的报错信息读出来，用string类型的变量_stderr来接收
                std::string _stderr;
                FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);
                out_value["stderr"] = _stderr;
                // 注意区分file_name.stderr与file_name.compile_error
                        // 这俩文件中记录的都是报错信息
                        // 只不过file_name.compile_error中记录的是编译时的报错信息
                        // 而file_name.stderr记录的是可执行程序运行时的报错信息
                        // 只有file_name.compile_error中没有报错信息，即成功编译，才能生成可执行文件，执行该文件才有可能会产生file_name.stderr中的报错信息
            }

            Json::StyledWriter writer;// Json::StyledWriter是 JsonCpp 提供的一个用于生成格式化 JSON 字符串的类
            *out_json = writer.write(out_value);
            // write()方法将out_value转换为格式化的 JSON 字符串，并返回该字符串。
            // out_json是一个指向std::string的指针（即std::string*）,通过解引用*out_json，将生成的 JSON 字符串赋值给out_json指向的对象。

            // 成功输出结果之后，我们需要清理在这个过程中产生的所有临时文件，否则产生堆积会造成空间的浪费
            RemoveTempFile(file_name);
        }
    };
}
